Skip to content

fix(server): stop per-session toolsets on session delete - #3884

Open
EronWright wants to merge 1 commit into
docker:mainfrom
EronWright:contrib/stop-toolsets-on-delete
Open

fix(server): stop per-session toolsets on session delete#3884
EronWright wants to merge 1 commit into
docker:mainfrom
EronWright:contrib/stop-toolsets-on-delete

Conversation

@EronWright

Copy link
Copy Markdown
Contributor

fix(server): stop per-session toolsets on session delete

Problem

A session materialised via teamloader owns a team whose toolsets may hold
external resources — notably stdio MCP subprocesses. DeleteSession cancels the
runtime context but never calls team.StopToolSets, and BatchDeleteSessions has
the same gap (LocalRuntime.Close only stops background agents; StopToolSets was
otherwise reached only via the transient GetAgentToolCount). So a deleted session's
MCP subprocesses leak until the server process exits.

Fix

Track the per-session team on activeRuntimes; once the session's stream has
drained, call StopToolSets from both delete paths. The drain runs in a background
goroutine that outlives the originating request, on a detached context
(WithoutCancel) carrying its own sessionDrainTimeout (5 min) budget — the same
budget already bounds the drain wait, so it now bounds the whole teardown (drain +
StopToolSets). Nil for attached runtimes (AttachRuntime), whose toolset lifecycle
belongs to the embedder — a no-op there.

Test

go test ./pkg/server/ green. Verified against a multi-tenant host: the per-session
subprocess count returns to zero on delete and after server shutdown (previously it
stayed pinned until exit).

A session materialised via teamloader owns a team whose toolsets may hold
external resources — notably stdio MCP subprocesses. DeleteSession cancelled
the runtime context but never called team.StopToolSets, so those subprocesses
leaked until the server process exited; BatchDeleteSessions had the same gap.

Track the per-session team on activeRuntimes and, once the session's stream
has drained, call StopToolSets from both delete paths, bounding it by the
originating request's deadline. Nil for attached runtimes (AttachRuntime),
whose toolset lifecycle belongs to the embedder, so it is a no-op there.
@EronWright
EronWright requested a review from a team as a code owner August 2, 2026 00:08
@aheritier aheritier added area/core Core agent runtime, session management kind/fix PR fixes a bug (maps to fix:). Use on PRs only. labels Aug 2, 2026
@aheritier

Copy link
Copy Markdown
Collaborator

The idea is interesting @EronWright Could you rebase your PR and validate that linters and tests are passing.
Before merging it I would like to get a review by a some people like @krissetto @simonferquel or @rumpl because even if the idea looks good I am not sure if it could have functionally some side effects in some cases

@aheritier
aheritier requested a review from docker-agent August 4, 2026 07:10

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

One medium-confidence finding was identified in the new code added by this PR.

}
}(sessionRuntime)
}
sm.runtimeSessions.Delete(sessionID)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] BatchDeleteSessions new drain goroutine cannot be awaited via WaitStopped

The PR adds an asynchronous goroutine in BatchDeleteSessions that waits for the session stream to drain and then calls sm.stopSessionToolSets — the same teardown work that DeleteSession does. However, unlike DeleteSession (which stores the runtime in deletedSessions before returning), BatchDeleteSessions never calls sm.deletedSessions.Store(sessionID, sessionRuntime).

WaitStopped reads exclusively from deletedSessions:

rs, ok := sm.deletedSessions.Load(sessionID)
if !ok {
    return nil // already cleaned up
}

So a caller that does BatchDeleteSessions(...) followed by WaitStopped(sessionID, ...) will receive an immediate nil even while the new drain goroutine is still running (waiting to drain the stream and call StopToolSets). Before this PR there was nothing to wait for in BatchDeleteSessions, but now there is — and the wait contract is silently broken.

Impact: Any consumer that uses WaitStopped to synchronise on full teardown (e.g. to confirm MCP subprocesses have exited) after a batch delete will get a false-complete signal. Consider mirroring DeleteSession's deletedSessions.Store call in BatchDeleteSessions (and having the drain goroutine call deletedSessions.Delete when done), or documenting that WaitStopped is not supported after batch deletes.

Confidence Score
🟡 moderate 65/100

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Core agent runtime, session management kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants